package org.pwwong.fxscalautils.components.containers

import com.sun.scenario.scenegraph.fx.FXGroup

import org.pwwong.fxscalautils.Predef._
import org.pwwong.fxscalautils.components.Component
import org.pwwong.fxscalautils.components.layouthints._

import org.pwwong.scalautils.property.ListProp
import org.pwwong.scalautils.property.listevents._
import org.pwwong.scalautils.signals.Predef._
import org.pwwong.scalautils.signals.{Signal,ConstantSignal,BindableSignal}
import org.pwwong.scalautils.time.Predef._

/**
 *  Base class for Components that contain other components.
 */
abstract class Container extends Component { self:Container =>
  type FXImpl = FXGroup
  compNodeImpl = new FXGroup
  
  final private val _children = ListProp[Component]
  final protected def setChildren(comps:Component*){ _children().clear; _children().insertAll(0,comps) }
  final def children:Seq[Component] = _children()
  final val onChildAdd = _children.onAdd
  final val onChildRemove = _children.onRemove
  final val onChildUpdate = _children.onUpdate
  final val onChildrenClear = _children.onClear
  
  //=== Layout ===//
  final protected[containers] val _containerLevel = BindableSignal[Int](-1)
  /**
   * The depth level of this Container.
   * The hierarchy of Newfangle Components and Containers is a Tree with Components = Leafs and Containers = Nodes.
   */
  final val containerLevel:Signal[Int] = _containerLevel
    
  final private val _isDirty = BindableSignal[Boolean](false)
  /**
   * Whether the Container is marked for a relayout.
   * This automatically is set to true whenever the following changes:
   *   1) Container's Size
   *   2) Child's Size
   *   3) Child's Preferred Size
   *   3) Child's Position
   *   4) Child's Anchor
   * 
   * This attribute is monitored by the LayoutManager.
   */
  final val isDirty:Signal[Boolean] = _isDirty
  
  /**
   * Marks the Container for relayout.
   */
  final def markDirty { _isDirty <= true }
  
  // Make sure relayout doesn't occur twice by different threads
  final private var isLayingOut:Boolean = false
  
  // Calls layout()
  final private[containers] def relayout {
    if(isLayingOut == false){
      LayoutManager.relayoutCount = LayoutManager.relayoutCount+1 
      isLayingOut = true
      layout()
      _isDirty <= false
      isLayingOut = false
    }
  }
  
  /**
   * Containers subclassing subclassing Container, must implement this method.
   * This method is in charge of setting this Container's children's current Size and Position, based on their Size and Position hints.
   * Helper methods setX, setY, setWidth, and setHeight should be used to set a child's current Size and Position.
   */
  protected def layout():Unit
  
  // Child bounds
  final protected def factorAnchorX(child:Component,newValue:Double):Double = (child.anchorX().origin match {
                                                                                 case LeftOrigin => 0.0
                                                                                 case RightOrigin => child.curWidth()
                                                                                 case CenterOrigin => child.curWidth()/2.0
                                                                              }) + 
                                                                              (child.anchorX().offset match {
                                                                                case PX(pxv) => pxv.toDouble
                                                                                case PCT(pctv) => pctv*child.curWidth()
                                                                              })
  
  final protected def factorAnchorY(child:Component,newValue:Double):Double = (child.anchorY().origin match {
                                                                                 case TopOrigin => 0.0
                                                                                 case BottomOrigin => child.curHeight()
                                                                                 case MiddleOrigin => child.curHeight()/2.0
                                                                              }) + 
                                                                              (child.anchorY().offset match {
                                                                                case PX(pxv) => pxv.toDouble
                                                                                case PCT(pctv) => pctv*child.curHeight()
                                                                              })
  /**
   * Subclasses should use this method to set a child's current X.
   */
  final protected def setX(child:Component)(newValue:Double){ child._x <= newValue }
  
  /**
   * Subclasses should use this method to set a child's current Y.
   */
  final protected def setY(child:Component)(newValue:Double){ child._y <= newValue }
  
  /**
   * Subclasses should use this method to set a child's current Width.
   */
  final protected def setWidth(child:Component)(newValue:Double){ child._width <= newValue }
  
  /**
   * Subclasses should use this method to set a child's current Height.
   */
  final protected def setHeight(child:Component)(newValue:Double){ child._height <= newValue }
  
  /**
   * Subclasses should use this method to set a child's current X and Y.
   */
  final protected def setXY(child:Component,newX:Double, newY:Double){ child._x <= newX; child._y <= newY }
  
  /**
   * Subclasses should use this method to set a child's current Width and Height.
   */
  final protected def setSize(child:Component,newW:Double, newH:Double){ child._width <= newW; child._height <= newH }
    
  /**
   * Called whenever a child is removed.
   * Subclasses can override this to react to child removal.
   */
  protected def handleChildRemove(index:Int, remComp:Component){
    // Unset Parent
    remComp._parent <= None
    
    // Remove from FX backing
    compNodeImpl.remove(remComp.compNodeImpl)
    
    // Remove Reactions
    List(remComp.preferredWidth, remComp.preferredWidth,
         remComp.x, remComp.y,
         remComp.w, remComp.h).foreach(_ removeListener this)
    
    markDirty
  } 
  
  /**
   * Called whenever a child is removed.
   * Subclasses can override this to react to child addition.
   */
  protected def handleChildAdd(index:Int, newComp:Component){
    // Remove old parent (if exists)
    newComp.parent().foreach(oldParent => oldParent._children().remove(oldParent._children().indexOf(newComp)))
    
    // Add to FX backing
    compNodeImpl.add(index,newComp.compNodeImpl) 
            
    // Add Reactions
    onAnyChanges(newComp.preferredWidth, newComp.preferredHeight,
                 newComp.x, newComp.y,
                 newComp.w, newComp.h)(this)( markDirty )
    
    // Set Parent (this)
    newComp._parent <= Some(this)
    
    markDirty
  }  
  
  /**
   * Called whenever all children are removed.
   * Subclasses can override this to react to all children being removed.
   */
  protected def handleChildrenCleared(){
    // Unset parent and Remove from FX backing
    while(compNodeImpl.getFXChildren.size>0) compNodeImpl.remove(0)
    markDirty
  }
  
  //== Reactions ==// 
  {import _children._
	  onAdd(this){ adev:AddEvent[Component] => handleChildAdd(adev.index, adev.elem) }
	  onRemove(this){ remev:RemoveEvent[Component] => handleChildRemove(remev.index, remev.elem) }
      onClear(this){ clearev:ClearEvent[Component] => handleChildrenCleared() }
  }
  
  // Calls markDirty whenever current size is changed (changed by parent container)
  onAnyChanges(curWidth,curHeight)(this)( markDirty )  // TODO fix clipping
  
  // Calls LayoutManager.requestRelayout marked dirty and not an orphan (containerLevel > -1) 
  isDirty.onChange(this){ dirty:Boolean => if(dirty && _containerLevel() > -1) LayoutManager.requestRelayout(this) }
  
  // Recalculates containerLevel (prent.containerLevel+1)
  parent.onChange(this){ p:Option[Container] => 
    if(p.isDefined) _containerLevel <= (eval { p.get.containerLevel()+1 } when(p.get.containerLevel))
    else            _containerLevel <= new ConstantSignal[Int](-1)
    LayoutManager.requestRelayout(this)
  }
}
